/*  SPDX-License-Identifier: GPL-2.0-or-later */
/*!********************************************************************

  Audacity: A Digital Audio Editor

  CloudSyncService.cpp

  Dmitry Vedenko

**********************************************************************/
#include "CloudSyncService.h"

#include <algorithm>
#include <cassert>
#include <chrono>
#include <mutex>
#include <string>

#include "CloudLibrarySettings.h"

#include "sync/CloudProjectsDatabase.h"
#include "sync/CloudSyncDTO.h"
#include "sync/RemoteProjectSnapshot.h"

#include "CodeConversions.h"

#include "OAuthService.h"
#include "ServiceConfig.h"

#include "MemoryX.h"

#include "Project.h"
#include "ProjectFileIO.h"

#include "BasicUI.h"
#include "FileNames.h"

#include "IResponse.h"
#include "NetworkManager.h"
#include "Request.h"

namespace audacity::cloud::audiocom {
namespace {
std::mutex& GetResponsesMutex()
{
    static std::mutex mutex;
    return mutex;
}

std::vector<std::shared_ptr<audacity::network_manager::IResponse> >&
GetPendingRequests()
{
    static std::vector<std::shared_ptr<audacity::network_manager::IResponse> >
    requests;

    return requests;
}

void RemovePendingRequest(audacity::network_manager::IResponse* request)
{
    std::lock_guard lock { GetResponsesMutex() };

    auto& requests = GetPendingRequests();

    requests.erase(
        std::remove_if(
            requests.begin(), requests.end(),
            [request](const auto& r) { return r.get() == request; }),
        requests.end());
}

void PerformProjectGetRequest(
    OAuthService& oAuthService, std::string url,
    std::function<void(ResponseResult)> dataCallback)
{
    assert(oAuthService.HasAccessToken());

    if (!oAuthService.HasAccessToken()) {
        dataCallback({ SyncResultCode::Unauthorized });
        return;
    }

    using namespace audacity::network_manager;

    auto request = Request(std::move(url));

    request.setHeader(
        common_headers::ContentType, common_content_types::ApplicationJson);

    request.setHeader(
        common_headers::Accept, common_content_types::ApplicationJson);

    SetCommonHeaders(request);
    SetOptionalHeaders(request);

    auto response = NetworkManager::GetInstance().doGet(request);

    response->setRequestFinishedCallback(
        [dataCallback = std::move(dataCallback)](auto response)
    {
        BasicUI::CallAfter(
            [dataCallback = std::move(dataCallback), response]
        {
            auto removeRequest
                =finally([response] { RemovePendingRequest(response); });

            dataCallback(GetResponseResult(*response, true));
        });
    });

    std::lock_guard lock { GetResponsesMutex() };
    GetPendingRequests().emplace_back(std::move(response));
}

void GetProjectInfo(
    OAuthService& oAuthService, const ServiceConfig& serviceConfig,
    std::string projectId,
    std::function<void(sync::ProjectInfo, ResponseResult)> callback)
{
    assert(callback);

    PerformProjectGetRequest(
        oAuthService, serviceConfig.GetProjectInfoUrl(projectId),
        [callback = std::move(callback)](ResponseResult result)
    {
        if (result.Code != SyncResultCode::Success) {
            callback(sync::ProjectInfo {}, std::move(result));
            return;
        }

        auto projectInfo = sync::DeserializeProjectInfo(result.Content);

        if (!projectInfo) {
            callback(
                {}, { SyncResultCode::UnexpectedResponse,
                      std::move(result.Content) });
            return;
        }

        callback(std::move(*projectInfo), std::move(result));
    });
}

void GetSnapshotInfo(
    OAuthService& oAuthService, const ServiceConfig& serviceConfig,
    std::string projectId, std::string snapshotId,
    std::function<void(sync::SnapshotInfo, ResponseResult result)> callback)
{
    assert(callback);

    PerformProjectGetRequest(
        oAuthService, serviceConfig.GetSnapshotInfoUrl(projectId, snapshotId),
        [callback = std::move(callback)](ResponseResult result)
    {
        if (result.Code != SyncResultCode::Success) {
            callback({}, std::move(result));
            return;
        }

        auto snapshotInfo = sync::DeserializeSnapshotInfo(result.Content);

        if (!snapshotInfo) {
            callback(
                {}, { SyncResultCode::UnexpectedResponse,
                      std::move(result.Content) });
            return;
        }

        callback(std::move(*snapshotInfo), std::move(result));
    });
}

void GetSnapshotInfo(
    OAuthService& oAuthService, const ServiceConfig& serviceConfig,
    std::string projectId, std::string snapshotId,
    std::function<
        void(sync::ProjectInfo, sync::SnapshotInfo, ResponseResult result)>
    callback)
{
    assert(callback);

    GetProjectInfo(
        oAuthService, serviceConfig, projectId,
        [callback   = std::move(callback), projectId,
         snapshotId = std::move(snapshotId), &oAuthService,
         &serviceConfig](sync::ProjectInfo projectInfo, ResponseResult result)
    {
        if (result.Code != SyncResultCode::Success) {
            callback({}, {}, std::move(result));
            return;
        }

        const auto id = !snapshotId.empty()
                        ? snapshotId
                        : (projectInfo.HeadSnapshot.Synced > 0
                           ? projectInfo.HeadSnapshot.Id
                           : projectInfo.LastSyncedSnapshotId);

        if (id.empty()) {
            callback(
                std::move(projectInfo), sync::SnapshotInfo {},
                { SyncResultCode::Success });
            return;
        }

        GetSnapshotInfo(
            oAuthService, serviceConfig, projectId, id,
            [callback    = std::move(callback),
             projectInfo = std::move(projectInfo)](
                sync::SnapshotInfo snapshotInfo, ResponseResult result)
        {
            callback(
                std::move(projectInfo), std::move(snapshotInfo),
                std::move(result));
        });
    });
}

bool HasAutosave(const std::string& path)
{
    auto connection = sqlite::Connection::Open(path, sqlite::OpenMode::ReadOnly);

    if (!connection) {
        return false;
    }

    if (!connection->CheckTableExists("autosave")) {
        return false;
    }

    auto statement
        =connection->CreateStatement("SELECT COUNT(1) FROM autosave");

    if (!statement) {
        return false;
    }

    auto result = statement->Prepare().Run();

    if (!result.IsOk()) {
        return false;
    }

    for (const auto& row : result) {
        if (row.GetOr(0, 0) > 0) {
            return true;
        }
    }

    return false;
}

bool DropAutosave(const std::string& path)
{
    auto connection
        =sqlite::Connection::Open(path, sqlite::OpenMode::ReadWrite);

    if (!connection) {
        return false;
    }

    if (!connection->CheckTableExists("autosave")) {
        return false;
    }

    auto statement = connection->CreateStatement("DELETE FROM autosave");

    if (!statement) {
        return false;
    }

    auto result = statement->Prepare().Run();

    return result.IsOk();
}
} // namespace

CloudSyncService& CloudSyncService::Get()
{
    static CloudSyncService service;
    return service;
}

CloudSyncService::GetProjectsFuture CloudSyncService::GetProjects(
    concurrency::CancellationContextPtr context, int page, int pageSize,
    std::string_view searchString)
{
    using namespace audacity::network_manager;

    auto promise = std::make_shared<GetProjectsPromise>();

    auto& serviceConfig = GetServiceConfig();
    auto& oAuthService  = GetOAuthService();

    auto request
        =Request(serviceConfig.GetProjectsUrl(page, pageSize, searchString));

    request.setHeader(
        common_headers::ContentType, common_content_types::ApplicationJson);
    request.setHeader(
        common_headers::Accept, common_content_types::ApplicationJson);

    SetCommonHeaders(request);
    SetOptionalHeaders(request);

    auto response = NetworkManager::GetInstance().doGet(request);

    context->OnCancelled(response);

    response->setRequestFinishedCallback(
        [promise, response](auto)
    {
        auto responseResult = GetResponseResult(*response, true);

        if (responseResult.Code != SyncResultCode::Success) {
            promise->set_value(responseResult);
            return;
        }

        auto projects
            =sync::DeserializePaginatedProjectsResponse(responseResult.Content);

        if (!projects) {
            promise->set_value(
                ResponseResult { SyncResultCode::UnexpectedResponse,
                                 std::move(responseResult.Content) });
            return;
        }

        promise->set_value(std::move(*projects));
    });

    return promise->get_future();
}

CloudSyncService::SyncFuture CloudSyncService::OpenFromCloud(
    std::string projectId, std::string snapshotId, SyncMode mode,
    sync::ProgressCallback callback)
{
    ASSERT_MAIN_THREAD();

    // Reset promise
    mSyncPromise = {};

    if (mSyncInProcess.exchange(true)) {
        CompleteSync({ sync::ProjectSyncResult::StatusCode::Blocked, {}, {} });
        return mSyncPromise.get_future();
    }

    if (projectId.empty()) {
        FailSync({ SyncResultCode::InternalClientError, "Empty projectId" });
        return mSyncPromise.get_future();
    }

    if (!callback) {
        mProgressCallback = [](auto...) { return true; } } else {
        mProgressCallback = std::move(callback);
    }

    GetSnapshotInfo(
        GetOAuthService(), GetServiceConfig(), projectId, snapshotId,
        [this, mode](
            sync::ProjectInfo projectInfo, sync::SnapshotInfo snapshotInfo,
            ResponseResult result)
    {
        if (result.Code != SyncResultCode::Success) {
            FailSync(std::move(result));
        } else {
            SyncCloudSnapshot(projectInfo, snapshotInfo, mode);
        }
    });

    return mSyncPromise.get_future();
}

CloudSyncService::SyncFuture CloudSyncService::SyncProject(
    AudacityProject& project, const std::string& path, bool forceSync,
    sync::ProgressCallback callback)
{
    ASSERT_MAIN_THREAD();

    // Reset promise
    mSyncPromise = {};

    if (mSyncInProcess.exchange(true)) {
        CompleteSync({ sync::ProjectSyncResult::StatusCode::Blocked });
        return mSyncPromise.get_future();
    }

    if (!callback) {
        mProgressCallback = [](auto...) { return true; } } else {
        mProgressCallback = std::move(callback);
    }

    auto& cloudDatabase = sync::CloudProjectsDatabase::Get();

    auto projectInfo = cloudDatabase.GetProjectDataForPath(path);

    if (!projectInfo) {
        // We assume, that the project is local
        CompleteSync(path);
        return mSyncPromise.get_future();
    }

    GetProjectInfo(
        GetOAuthService(), GetServiceConfig(), projectInfo->ProjectId,
        [projectInfo = *projectInfo, &project, path,
         mode        = forceSync ? SyncMode::ForceOverwrite : SyncMode::Normal,
         this](sync::ProjectInfo remoteInfo, ResponseResult result)
    {
        if (result.Code != SyncResultCode::Success) {
            FailSync(std::move(result));
            return;
        }

        // Do not perform the snapshot request if the project is up to date
        if (remoteInfo.HeadSnapshot.Id == projectInfo.SnapshotId) {
            CompleteSync({ sync::ProjectSyncResult::StatusCode::Succeeded });
            return;
        }

        const auto snapshotId = remoteInfo.HeadSnapshot.Synced > 0
                                ? remoteInfo.HeadSnapshot.Id
                                : remoteInfo.LastSyncedSnapshotId;

        GetSnapshotInfo(
            GetOAuthService(), GetServiceConfig(), remoteInfo.Id, snapshotId,
            [this, &project, mode,
             remoteInfo](sync::SnapshotInfo snapshotInfo, ResponseResult result)
        {
            if (result.Code != SyncResultCode::Success) {
                FailSync(std::move(result));
            } else {
                SyncCloudSnapshot(remoteInfo, snapshotInfo, mode);
            }
        });
    });

    return mSyncPromise.get_future();
}

bool CloudSyncService::IsCloudProject(const std::string& path)
{
    auto& cloudDatabase = sync::CloudProjectsDatabase::Get();
    auto projectInfo    = cloudDatabase.GetProjectDataForPath(path);

    return projectInfo.has_value();
}

CloudSyncService::ProjectState
CloudSyncService::GetProjectState(const std::string& projectId)
{
    auto& cloudDatabase = sync::CloudProjectsDatabase::Get();

    auto projectInfo = cloudDatabase.GetProjectData(projectId);

    if (!projectInfo) {
        return ProjectState::NotAvaliable;
    }

    if (!wxFileExists(ToWXString(projectInfo->LocalPath))) {
        return ProjectState::NotAvaliable;
    }

    if (projectInfo->SyncStatus == sync::DBProjectData::SyncStatusSynced) {
        return ProjectState::FullySynced;
    }

    return ProjectState::PendingSync;
}

CloudSyncService::GetHeadSnapshotIDFuture
CloudSyncService::GetHeadSnapshotID(std::string_view projectId)
{
    ASSERT_MAIN_THREAD();

    auto promise = std::make_shared<GetHeadSnapshotIDPromise>();

    GetProjectInfo(
        GetOAuthService(), GetServiceConfig(), std::string(projectId),
        [promise](sync::ProjectInfo projectInfo, ResponseResult result)
    {
        if (result.Code != SyncResultCode::Success) {
            promise->set_value(
                sync::GetHeadSnapshotIDResult { std::move(result) });
        } else {
            promise->set_value({ projectInfo.HeadSnapshot.Id });
        }
    });

    return promise->get_future();
}

void CloudSyncService::FailSync(ResponseResult responseResult)
{
    CompleteSync(
        sync::ProjectSyncResult { sync::ProjectSyncResult::StatusCode::Failed,
                                  std::move(responseResult),
                                  {} });
}

void CloudSyncService::CompleteSync(std::string path)
{
    CompleteSync(sync::ProjectSyncResult {
            sync::ProjectSyncResult::StatusCode::Succeeded, {}, std::move(path) });
}

void CloudSyncService::CompleteSync(sync::ProjectSyncResult result)
{
    if (mRemoteSnapshot) {
        result.Stats = mRemoteSnapshot->GetTransferStats();
        ReportUploadStats(mRemoteSnapshot->GetProjectId(), result.Stats);
    }

    mSyncPromise.set_value(std::move(result));
    mRemoteSnapshot.reset();
    mSyncInProcess.store(false);
}

void CloudSyncService::SyncCloudSnapshot(
    const sync::ProjectInfo& projectInfo, const sync::SnapshotInfo& snapshotInfo,
    SyncMode mode)
{
    // Get the project location
    auto localProjectInfo
        =sync::CloudProjectsDatabase::Get().GetProjectData(projectInfo.Id);

    const auto createNew = !localProjectInfo || mode == SyncMode::ForceNew;

    const auto wxPath = createNew
                        ? sync::MakeSafeProjectPath(
        CloudProjectsSavePath.Read(),
        audacity::ToWXString(projectInfo.Name))
                        : audacity::ToWXString(localProjectInfo->LocalPath);

    const auto utf8Path = audacity::ToUTF8(wxPath);

    const auto fileExists = wxFileExists(wxPath);

    if (!fileExists) {
        if (snapshotInfo.Id.empty()) {
            FailSync({ SyncResultCode::SyncImpossible });
            return;
        }

        const auto dir = CloudProjectsSavePath.Read();
        FileNames::MkDir(dir);

        InvisibleTemporaryProject project;
        ProjectFileIO::Get(project.Project()).LoadProject(wxPath, true);
    } else {
        assert(localProjectInfo.has_value());
        assert(mode != SyncMode::ForceNew);
        // The project exists on the disk. Depending on how we got here, we might
        // different scenarios:
        // 1. Local snapshot ID matches the remote snapshot ID. Just complete the
        // sync right away. If the project was modified locally, but not saved,
        // the user will be prompted about the autosave.
        if (
            localProjectInfo->SnapshotId == snapshotInfo.Id
            && localProjectInfo->SyncStatus != sync::DBProjectData::SyncStatusDownloading) {
            CompleteSync(
                { sync::ProjectSyncResult::StatusCode::Succeeded, {}, utf8Path });
            return;
        }
        // 2. Project sync was interrupted.
        if (
            mode == SyncMode::Normal
            && localProjectInfo->SyncStatus == sync::DBProjectData::SyncStatusUploading) {
            // There is not enough information to decide if the project has
            // diverged. Just open it, so the sync can resume. If the project has
            // diverged, the user will be prompted to resolve the conflict on the
            // next save.
            CompleteSync(
                { sync::ProjectSyncResult::StatusCode::Succeeded, {}, utf8Path });
            return;
        }
        // 3. Project was modified locally, but not saved.
        if (HasAutosave(utf8Path)) {
            if (mode == SyncMode::Normal) {
                FailSync({ SyncResultCode::Conflict });
                return;
            } else {
                DropAutosave(utf8Path);
            }
        }
    }

    if (snapshotInfo.Id.empty()) {
        FailSync({ SyncResultCode::SyncImpossible });
        return;
    }

    mRemoteSnapshot = sync::RemoteProjectSnapshot::Sync(
        projectInfo, snapshotInfo, utf8Path,
        [this, createNew, path = utf8Path, projectId = projectInfo.Id,
         snapshotId = snapshotInfo.Id](sync::RemoteProjectSnapshotState state)
    {
        UpdateDowloadProgress(
            (state.ProjectDownloaded + state.BlocksDownloaded)
            / (state.BlocksTotal + 1.0));

        if (state.IsComplete()) {
            const bool success = state.Result.Code == SyncResultCode::Success;

            CompleteSync({ success
                           ? sync::ProjectSyncResult::StatusCode::Succeeded
                           : sync::ProjectSyncResult::StatusCode::Failed,
                           std::move(state.Result), std::move(path) });
        }
    },
        mode == SyncMode::ForceNew);
}

void CloudSyncService::UpdateDowloadProgress(double downloadProgress)
{
    mDownloadProgress.store(downloadProgress);

    if (mProgressUpdateQueued.exchange(true)) {
        return;
    }

    BasicUI::CallAfter(
        [this]
    {
        auto remoteSnapshot = mRemoteSnapshot;

        if (remoteSnapshot && !mProgressCallback(mDownloadProgress.load())) {
            remoteSnapshot->Cancel();
        }

        mProgressUpdateQueued.store(false);
    });
}

void CloudSyncService::ReportUploadStats(
    std::string_view projectId, const TransferStats& stats)
{
    sync::NetworkStats networkStats;

    networkStats.Bytes      = stats.BytesTransferred;
    networkStats.Blocks     = stats.BlocksTransferred;
    networkStats.Files      = stats.ProjectFilesTransferred;
    networkStats.Mixes      = 0;
    networkStats.IsDownload = true;

    using namespace audacity::network_manager;

    auto request = Request(GetServiceConfig().GetNetworkStatsUrl(projectId));

    request.setHeader(
        common_headers::ContentType, common_content_types::ApplicationJson);

    SetCommonHeaders(request);
    SetOptionalHeaders(request);

    auto body = Serialize(networkStats);

    auto response
        =NetworkManager::GetInstance().doPost(request, body.data(), body.size());
    // Keep response alive
    response->setRequestFinishedCallback([response](auto) {});
}
} // namespace audacity::cloud::audiocom
